在 Day 13 中,我們成功鋪設了深藍紫色的冷調環境底光,並透過高解析度的 PCF 軟陰影映射,讓斜射的月光在廢墟中拉出修長的陰影,奠定了地窖深邃神祕的氛圍。然而,如果整個場景只有冰冷的月光,這座古堡看起來就會像是一座毫無生機的死城,缺乏吸引玩家探索的「冒險感」。
在經典的 HD-2D 美術設計中,最迷人的畫面往往建立在強烈的「冷暖對比(Chiaroscuro)」之上——在幽暗寒冷的迷霧中,點綴幾盞散發著橘黃色暖光的火把或壁燈。今天,我們將在 Day 12 預先記錄的牆壁錨點上,安裝實體的鍛鐵壁燈,並透過純數學的三角函數疊加演算法,為場景注入充滿生命力的「程序化燭火搖曳(Procedural Flicker)」動態特效!
我們不能只在空中放一個看不見的光源,那樣會非常沒有真實感。我們需要一個實體的 3D 模型來承載這股火光。 在 src/main.js 中,我們宣告了一個小巧的長方體作為壁燈本體,長寬高分別設定為 0.35 x 0.45 x 0.35。最關鍵的是,我們為壁燈材質啟動了「自發光(Emissive)」屬性:
const lanternGeo = new THREE.BoxGeometry(0.35, 0.45, 0.35);
const lanternMat = new THREE.MeshStandardMaterial({
map: lanternTex,
emissive: new THREE.Color('#ff8822'),
emissiveIntensity: 1.6,
roughness: 0.4,
metalness: 0.3
});
有了壁燈模型後,我們需要真正能照亮周圍牆壁的「點光源(PointLight)」。 我們透過 forEach 迴圈遍歷 Day 12 儲存的 lanternPositions 陣列,精準計算出每一盞燈的三維座標。高度 ly 設定為牆壁高度 h + 0.25,剛好讓壁燈安裝在牆頂微凸的位置
const animatedLights = [];
lanternPositions.forEach((pos, idx) => {
const h = wallHeightMap[pos.r]?.[pos.c] || 2;
const lx = pos.c - (mapSize / 2) + 0.5;
const lz = pos.r - (mapSize / 2) + 0.5;
const ly = h + 0.25; // 安裝在牆頂微凸處
// 3D 壁燈本體
const lanternMesh = new THREE.Mesh(lanternGeo, lanternMat);
lanternMesh.position.set(lx, ly, lz);
lanternMesh.castShadow = true;
scene.add(lanternMesh);
// 溫暖的橘黃色地城火光
const pointLight = new THREE.PointLight('#ff9d3a', 3.8, 9, 1.4);
pointLight.position.set(lx, ly + 0.2, lz);
// 前 4 盞主要壁燈啟用投影,兼顧畫質與極致效能
if (idx < 4) {
pointLight.castShadow = true;
pointLight.shadow.bias = -0.002;
pointLight.shadow.mapSize.width = 512;
pointLight.shadow.mapSize.height = 512;
}
scene.add(pointLight);
真實的火把光芒絕對不會是恆定不變的,它會隨著地城微風的吹拂而閃爍搖動。如果沒有動態閃爍,畫面會顯得很死板。
在生成每一盞燈時,我們不僅將它加入場景,還會把它的參考物件與隨機變數推入 animatedLights 陣列中儲存:
animatedLights.push({
light: pointLight,
baseIntensity: 3.8,
speed: 4 + Math.random() * 3,
phase: Math.random() * Math.PI * 2
});
接著,我們進入核心渲染迴圈 animate,利用時間與三角函數,實作極富生機的程序化動畫:
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
// 壁燈燭火微動搖曳 (Procedural Torch Flicker)
animatedLights.forEach(item => {
const flicker = Math.sin(elapsedTime * item.speed + item.phase) * 0.35 +
Math.cos(elapsedTime * item.speed * 2.1 + item.phase) * 0.2;
item.light.intensity = item.baseIntensity + flicker;
});
當你完成這段程式碼並刷新網頁時,魔法就發生了!幽暗的地城被幾盞散發著橘黃色暖光的壁燈點亮,火光在冷藍色的石牆上跳動搖曳,將地城襯托得無比深邃且充滿活著的氣息。
【本日結語】
這座專屬於你的 HD-2D 地宮,在場景建構與光影表現上已經正式完工。明天Day 15,也就是我們場景的最後一步,我們將把這一切打包進最終的運鏡迴圈,完成相機的精準鎖定機制,準備迎接主角的華麗登場,同時也會附上完整程式碼以及實際的場景畫面,大家敬請期待。